using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using msilgc.common;

namespace msilgc.driver.cuda.Compilers
{
	internal class UnixCpp : INativeCompiler
	{
		public UnixCpp ()
		{
		}

	    private static string Gcc {
	        get {
	            var result = EnvironmentFinder.GetValueTryDefault(
	                new[] {"CC", "cc"},
	                new[] {"/usr/bin/g++", "/usr/local/bin/g++", }
	                );
	            return result;
	        }
	    }

	    private static string IncludePath {
	        get {
                var result = EnvironmentFinder.GetValueTryDefault(
	                new[] {"inc"},
	                new[] {"/usr/include", "/usr/local/include"});
	            return result;
	        }
	    }

		public void RunCompiler (DirectoryInfo dllOutputPath, IEnumerable<FileInfo> kernelFiles, out string compilerMsgs)
		{
            compilerMsgs = "";
            foreach (var kernelFile in kernelFiles) {
                string msg;
                RunCompiler(dllOutputPath, kernelFile, out msg);
                compilerMsgs += Environment.NewLine + msg;
            }
		}

        public void RunCompiler(DirectoryInfo dllOutputPath, FileInfo kernelFile, out string compilerMsgs) {
            Logger.WriteLine("\tC++ Compiling...");
            using (var nvcc = new Process()) {
                nvcc.StartInfo.FileName = Path.Combine(CudaSettings.CudaBinPath, "nvcc");
                nvcc.StartInfo.Arguments = string.Format(
                    "-ccbin {0} -c -Dinline=__inline -DUnix -D_MBCS -Xcompiler -fpic -O2 -c -I{1} -I{2} -o {3} {4}",
                    Pad(Gcc),
                    Pad(CudaSettings.CudaIncludePath),
                    Pad(IncludePath),
                    Pad(Path.Combine(dllOutputPath.FullName, kernelFile.Name.Replace(".cu", ".o"))),
                    Pad(Path.Combine(dllOutputPath.FullName, kernelFile.Name))
                    );

                nvcc.StartInfo.RedirectStandardError = true;
                nvcc.StartInfo.RedirectStandardOutput = true;
                nvcc.StartInfo.UseShellExecute = false;
                nvcc.StartInfo.CreateNoWindow = true;

                if (!nvcc.Start()) {
                    throw new Exception("compiler was not started");
                }

                using (var reader = new ProcessOutputReader(nvcc)) {
                    if (!nvcc.WaitForExit(30000)) {
                        nvcc.Kill();
                        throw new Exception("compiler didn't exit within 30 seconds");
                    }
                    compilerMsgs = reader.GetOutput();
                }
                if (!File.Exists(Path.Combine(dllOutputPath.FullName, kernelFile.Name.Replace(".cu", ".o"))) || (nvcc.ExitCode != 0)) {
                    throw new Exception(
                        "failed to compile.\r\ncompiler output: " + compilerMsgs +
                        "\r\ncompiler command line: " + nvcc.StartInfo.Arguments);
                }
            }
        }

        private static string Pad(string path) {
            if (path.Contains(" ")) {
                return string.Concat("\"", path, "\"");
            }
            return path;
        }

		public void RunLinker (DirectoryInfo dllOutputPath, IEnumerable<string> objectFileBaseNames) {
		    Logger.WriteLine("\tC++ Linking...");
            if (!objectFileBaseNames.Any()) {
                throw new ArgumentOutOfRangeException("objectFileBaseNames", "No objects passed");
            }
            using (var gcc = new Process()) {
                var objects =
                    objectFileBaseNames.Select(x => string.Concat(x, ".o"))
                                       .Select(Pad)
                                       .Aggregate((x, y) => string.Concat(x, " ", y));
                gcc.StartInfo.FileName = Gcc;
                gcc.StartInfo.Arguments = string.Format(
                    "-shared -o {0} {1}",
                    Pad(Path.Combine(dllOutputPath.FullName, "libdriver.so")),
                    objects);
                gcc.StartInfo.RedirectStandardError = true;
                gcc.StartInfo.RedirectStandardOutput = true;
                gcc.StartInfo.UseShellExecute = false;
                gcc.StartInfo.CreateNoWindow = true;

                //Logger.WriteLine("arguments: {0}", gcc.StartInfo.Arguments);
                
                if (!gcc.Start()) {
                    throw new Exception("linker was not started");
                }

                using (var reader = new ProcessOutputReader(gcc)) {
                    if (!gcc.WaitForExit(30000)) {
                        gcc.Kill();
                        throw new Exception("linker didn't exit within 30 seconds");
                    }
                    if (gcc.ExitCode != 0) {
                        throw new Exception(
                            "linker failed, arguments: " + 
                            gcc.StartInfo.Arguments + Environment.NewLine +
                            reader.GetOutput());
                    }
                }
            }
		}
	}
}

